#!/usr/bin/env python
# Copyright (c) 2007-2008 University of Alaska Fairbanks
# This software is made available under the BSD license provided in the LICENSE
# file included with this distribution.

"""
pyscene.py - friendly wrapper for model navigation and peripheral interaction
    to OpenSceneGraph through osgswig with python
"""

import traceback, re, os, sys, math

class pyscene(object) :
    def __init__(self) :
        self.lighting = False
        
        import osg, osgViewer, osgGA
        self.viewer = osgViewer.Viewer()
        self.viewer.setThreadingModel(self.viewer.SingleThreaded)
        self.root = osg.Group()
        self.viewer.setSceneData(self.root)
        
        self.mm = osgGA.TrackballManipulator()
        self.mm.setCenter( osg.Vec3d() )
        self.mm.home(0.0)
        self.viewer.setCameraManipulator(self.mm)
        
        self.ds = osg.DisplaySettings()
        self.viewer.setDisplaySettings(self.ds)
        
        self.nav_scale = {}
        self.velocity = osg.Matrixd()
        self.button_velocity = osg.Matrixd()
        
        self.wand_matrix = osg.Matrixd()
        self.vrpn_handlers = {}
        
        self.setScale(1.0)
        self.parse_args()
   
        for ref in pyscene, osg.Node, osg.Geode, osg.MatrixTransform :
            def _addUpdateCallback(self, f) :
                if self.getUpdateCallback() == None :
                    class CB(osg.NodeCallback) :
                        def __init__(cb) :
                            import osg
                            cb.methods = []
                            osg.NodeCallback.__init__(cb)
                        def __call__(cb, node, nv) :
                            for method in cb.methods :
                                method(self, node, nv)
                            node.traverse(nv)
                    self.__update_callback__ = CB()
                    self.setUpdateCallback(self.__update_callback__)
                self.__update_callback__.methods.append(f)
            setattr(ref, "addUpdateCallback", _addUpdateCallback)
            
            def _removeUpdateCallback(self, f) :
                self.__update_callback__.methods = filter(
                    lambda x : x != f, self.__update_callback__.methods
                )
            setattr(ref, "removeUpdateCallback", _removeUpdateCallback)
        
    def parse_args(self) :
        """
            Look in the command line arguments for models, configuration files,
            and options, loading files and setting options as necessary.
        """
        import getopt
    
        # hack away at argv to supply default arguments
        argv = []
        for c in sys.argv[1:] :
            if c == "--stereo" :
                argv.append("--stereo=anaglyphic")
            elif c == "--snow" :
                argv.append("--snow=0.5")
            elif c == "--rain" :
                argv.append("--rain=0.5")
            elif c == "--lighting" :
                argv.append("--lighting=true")
            else :
                argv.append(c)
         
        optlist, args = getopt.gnu_getopt(
            argv, "", """
                stereo= snow= rain= lighting= vrpn= navigation=
                window= width= height= distance=
            """.split()
        )
        opts = dict(optlist)
        
        if opts.has_key("--stereo") :
            self.ds.setStereo(True)
            if re.search("ana", opts["--stereo"], re.I) :
                self.ds.setStereoMode(self.ds.ANAGLYPHIC)
            elif re.search("quad", opts["--stereo"], re.I) :
                self.ds.setStereoMode(self.ds.QUAD_BUFFER)
            elif re.search("horiz", opts["--stereo"], re.I) :
                self.ds.setStereoMode(self.ds.HORIZONTAL_SPLIT)
            elif re.search("vert", opts["--stereo"], re.I) :
                self.ds.setStereoMode(self.ds.VERTICAL_SPLIT)
        
        lighting = False
        if opts.has_key("--lighting") :
            if re.match("^ (?: false | no )", opts["--lighting"], re.I | re.X) :
                lighting = False
            else :
                lighting = True
        
        if opts.has_key("--snow") :
            self.wx( "snow", float(opts["--snow"]) )

        if opts.has_key("--rain") :
            self.wx( "rain", float(opts["--rain"]) )
        
        self.navigation_mode = "fly"
        if opts.has_key("--navigation") :
            self.navigation_mode = opts["--navigation"]
        
        config = []
        self.vrpn_host = "localhost:4321"
        if opts.has_key("--vrpn") :
            self.vrpn_host = opts["--vrpn"]
        
        for arg in args :
            if re.search(r"\. cfg $", arg, re.X) :
                config.append(arg)
            elif re.search(r"\. ( osg | obj | ive? ) $", arg, re.X) :
                # Load models passed in the command line arguments
                import osg
                model = self.readNodeFile(arg)
                GL_LIGHTING = 2896
                ss = model.getOrCreateStateSet()
                if lighting == True :
                    ss.setMode(GL_LIGHTING, osg.StateAttribute.ON)
                elif lighting == False :
                    ss.setMode(GL_LIGHTING, osg.StateAttribute.OFF)
                self.addChild(model)
            
        self.devices = {}
        self.ports = {}
        for c in config :
            # Look for enabled devices in configuration file
            fh = open(c, "r")
            for line in fh :
                joy_matches = re.match(
                    r"^ \s* vrpn_Joy(?: lin | stick) \s+ (\S+) \s+ (\S+)",
                    line, re.X
                )
                if joy_matches :
                    dev, file = joy_matches.groups()
                    self.devices[ "%s@%s" % (dev, self.vrpn_host) ] = file
                
                ws_matches = re.match(
                    r"^ ( wand | stylus ) \s+ (\S+) \s+ (\d+)",
                    line, re.X | re.I
                )
                if ws_matches :
                    dev_type = ws_matches.group(1).lower()
                    dev = ws_matches.group(2)
                    # device identifier => device type
                    self.devices[ "%s@%s" % (dev, self.vrpn_host) ] = dev_type
                    # device type => port number
                    self.ports[dev_type] = ws_matches.group(3)
                tracker_matches = re.match(
                    r"^ vrpn_tracker_intersense \s+ (\S+)",
                    line, re.X | re.I
                )
                if tracker_matches :
                    dev = tracker_matches.group(1)
                    dev_type = "tracker"
                    # device identifier => device type
                    self.devices[ "%s@%s" % (dev, self.vrpn_host) ] = dev_type
                    # device type => port number
                    self.ports[ "%s@%s" % (dev, self.vrpn_host) ] = "0"
                
            fh.close()
        
    def add_vrpn_handler(self, name, func) :
        if not callable(func) :
            raise RuntimeError("VRPN handler '%s' is not callable" % func)
        self.vrpn_handlers[name] = func
        
    def process_opts(self) :
        
        def velocity_callback(self, node, nv) :
            import osg
            if self.velocity != osg.Matrixd.identity() :
                self.postMult(self.velocity)
            if self.button_velocity != osg.Matrixd.identity() :
                self.postMult(self.button_velocity)
        self.addUpdateCallback(velocity_callback)
        
        if len(self.devices) == 0 :
            print "No VRPN devices found, assuming mouse/keyboard input"
        else :
            plugin_list = """
                logitech_rumble_pad.py
                wand.py
                saitek_rumble_pad.py
                tracker.py
                saitek_joystick.py
                logitech_rumble_pad.pyc
                wand.pyc
                saitek_rumble_pad.pyc
                tracker.pyc
                saitek_joystick.pyc
            """.split()

            #print "%s/plugins/navigation" % os.getcwd()
            #for f in os.listdir("%s/plugins/navigation" % os.getcwd()) :
            for f in plugin_list :
                if re.match("^_", f) : continue
                m = re.match("(.+) \.py $", f, re.X)
                if not m : continue
                name = m.group(1)
                
                plugin = __import__(
                    "plugins.navigation.%s" % name,
                    globals(), locals(), name,
                )
                
                for dev in self.devices.values() :
                    if plugin.matches(dev, self.navigation_mode) :
                        self.add_vrpn_handler(name, plugin.handler)
                        print "Using VRPN plugin %s" % name
                        break
            
            from plugins.vrpn import vrpn
            self.vrpn_clients = []
            for dev in self.devices :
                self.vrpn_clients.append( vrpn(dev, host=self.vrpn_host) )
            
            def vrpn_callback(self, node, nv) :
                for client in self.vrpn_clients :
                    try :
                        client.populate()
                        event = client.pop()
                        if event != None :
                            for handler in self.vrpn_handlers.values() :
                                handler(self, event)
                    except :
                        traceback.print_exc()
                        self.viewer.setDone(True)
            #self.addUpdateCallback(vrpn_callback)
            import threading
            def vrpn_thread() :
                while not self.viewer.done() :
                    vrpn_callback(self, None, None)
            threading.Thread(None, vrpn_thread).start()
            
    def setScale(self, t_scale, r_scale=1.0) :
        t_scale *= 0.5
        r_scale *= 0.004
        if r_scale == None :
            r_scale = t_scale
        import osg
        if isinstance(t_scale, ( osg.Vec3d, osg.Vec3f, osg.Vec3 )) :
            self.nav_scale["translation"] = t_scale
        elif isinstance(t_scale, ( list, tuple )) :
            self.nav_scale["translation"] = apply(osg.Vec3d, t_scale)
        else :
            self.nav_scale["translation"] = osg.Vec3d(t_scale, t_scale, t_scale)
        
        self.nav_scale["rotation"] = r_scale
        
        self.ds.setScreenWidth(9.144)
        self.ds.setScreenHeight(2.4384)
        self.ds.setScreenDistance(4.572)
        self.ds.setEyeSeparation(0.01)
        self.viewer.setFusionDistance(
            self.viewer.getFusionDistanceMode(), 1.0
        )
            
    def postMult(self, m) :
        return self.setViewMatrix( m * self.getViewMatrix() )
    
    def preMult(self, m) :
        return self.setViewMatrix( self.getViewMatrix() * m )
    
    def getViewMatrix(self) :
        return self.mm.getMatrix()
    
    def setViewMatrix(self, m) :
        self.mm.setCenter( m.getRotate() * m.getTrans() )
        self.mm.home(0.0)
        self.mm.setByMatrix(m)
     
    def getPosition(self) :
        return self.getViewMatrix().getTrans()
    
    def addChild(self, node) :
        return self.root.addChild(node)
    
    def getNumChildren(self) :
        return self.root.getNumChildren()
    
    def getChild(self, n) :
        return self.root.getChild(n)
    
    def removeChild(self, node) :
        return self.root.removeChild(node)
    
    def setLighting(self, mode) :
        import osg
        for i in range( self.root.getNumChildren() ) :
            GL_LIGHTING = 2896
            ss = self.root.getChild(i).getOrCreateStateSet()
            if mode == True :
                ss.setMode(GL_LIGHTING, osg.StateAttribute.ON)
            elif mode == False :
                ss.setMode(GL_LIGHTING, osg.StateAttribute.OFF)
     
    def readNodeFile(self, filename) :
        import osgDB
        return osgDB.readNodeFile(filename)
    
    def getUpdateCallback(self) :
        return self.root.getUpdateCallback()
    
    def setUpdateCallback(self, cb) :
        return self.root.setUpdateCallback(cb)
    
    def run(self) :
        try :
            self.process_opts()
            self.viewer.run()
        except :
            print "Caught exception"
            traceback.print_exc()
            self.viewer.setDone(True)
            
        try :
            del self.vrpn_clients
        except : None
        
    # Fancy conveniences
    from plugins.wx import wx
    from plugins.ic import ic
    
# Go through the motions like osgviewer if invoked directly
if __name__ == "__main__" :
    # Set all the include paths based on the file name
    # No more messing with silly environment variables
    import os, sys, re
    pyscene_path = os.path.dirname(os.path.realpath(__file__))
    sys.path.append(pyscene_path)
    sys.path.append("%s/lib/osgswig" % pyscene_path)
    os.putenv("PATH", os.getenv("PATH") + ":%s/bin" % pyscene_path)
    os.putenv("LD_LIBRARY_PATH",
        (
            os.getenv("LD_LIBRARY_PATH") or "/lib:/usr/lib:/usr/local/lib"
        ) + ":%s/lib" % pyscene_path
    )
    
    for arg in sys.argv[1:] :
        # Run any python scripts specified as arguments
        if not re.search(r"\. py $", arg, re.X) : continue
        # Prevent infinite recursion:
        if os.path.realpath(arg) == os.path.realpath(__file__) : continue
        execfile(arg)
        break
    else :
        scene = pyscene()
        if scene.root.getNumChildren() > 0 :
            scene.run()
        else :
            # Launch into interactive console if no models loaded
            import code, readline
            import osg, osgDB, osgViewer, osgGA, osgUtil
            code.InteractiveConsole( locals() ).interact(
                "[ pyscene3.0 interactive console ]"
            )

